<?php
/**
 * Nuwani PHP IRC Bot Framework
 * Copyright (c) 2006-2010 The Nuwani Project
 *
 * Nuwani is a framework for IRC Bots built using PHP. Nuwani speeds up bot 
 * development by handling basic tasks as connection- and bot management, timers
 * and module managing. Features for your bot can easily be added by creating
 * your own modules, which will receive callbacks from the framework.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @copyright Copyright (c) 2006-2011 The Nuwani Project
 * @author Peter Beverloo <peter@lvp-media.com>
 * @author Dik Grapendaal <dik.grapendaal@gmail.com>
 * @see http://nuwani.googlecode.com
 */

//namespace Nuwani;

require __DIR__ . '/UnoStrings.php';
require __DIR__ . '/UnoInput.php';
require __DIR__ . '/UnoOutput.php';
require __DIR__ . '/UnoCard.php';
require __DIR__ . '/UnoDeckCard.php';
require __DIR__ . '/UnoCardStack.php';
require __DIR__ . '/UnoPlayer.php';

/**
 * Uno Nuwani Module
 * 
 * @copyright Copyright (c) 2011 The Nuwani Project, http://nuwani.googlecode.com/
 * @author Dik Grapendaal <dik.grapendaal@gmail.com>
 * @version $Id: UnoEngine.php 148 2011-07-26 19:10:23Z dik.grapendaal $
 * @package Modules
 */

class UnoEngine
{
        /**
         * Settings.
         */
        
        const   SETTING_CARDS_PER_PLAYER        = 10;
        const   SETTING_DRAW_SKIPS              = false; // todo
        const   SETTING_SORT_CARDS              = true;
        const   SETTING_STRICT_WILDCARD         = false; // todo
        
        /**
         * These states indicate the state of the engine. Idle means that
         * nothing is going on, and that a new game can be started. Pending
         * means that it's waiting for action, mostly waiting for players to
         * join. Finally, playing means that a game is in progress.
         */
        
        const   STATE_IDLE                      = 0;
        const   STATE_PENDING                   = 1;
        const   STATE_INITIALIZING              = 2;
        const   STATE_PLAYING                   = 3;
        
        /**
         * These constants define ways of inputting cards.
         */
        
        const   INPUT_SINGLE                    = 0;
        const   INPUT_STACK                     = 1;
        
        /**
         * The three indexes in the array used to indicate a played card or
         * stack of cards.
         */
        
        const   INDEX_INPUT                     = 0;
        const   INDEX_COLOR                     = 1;
        const   INDEX_NUMBER                    = 2;
        const   INDEX_NEXT                      = 3;
        
        /**
         * The available colors for the cards.
         */
        
        const   COLOR_BLUE                      = 0;
        const   COLOR_GREEN                     = 1;
        const   COLOR_RED                       = 2;
        const   COLOR_YELLOW                    = 3;
        const   COLOR_WILDCARD                  = 4;
        
        /**
         * The available cards.
         */
        
        const   CARD_NONE                       = 0;
        const   CARD_0                          = 1;
        const   CARD_1                          = 2;
        const   CARD_2                          = 3;
        const   CARD_3                          = 4;
        const   CARD_4                          = 5;
        const   CARD_5                          = 6;
        const   CARD_6                          = 7;
        const   CARD_7                          = 8;
        const   CARD_8                          = 9;
        const   CARD_9                          = 10;
        const   CARD_DRAW_2                     = 11;
        const   CARD_DRAW_4                     = 12;
        const   CARD_REVERSE                    = 13;
        const   CARD_SKIP                       = 14;
        
        /**
         * The object where the engine gets its input from.
         * 
         * @var UnoInput
         */
        
        private $mInput;
        
        /**
         * The object where the engine sends its output to.
         * 
         * @var UnoOutput
         */
        
        private $mOutput;
        
        /**
         * The state the engine currently resides in.
         * 
         * @var integer
         */
        
        private $mState;
        
        /**
         * The start time of the game.
         * 
         * @var integer
         */
        
        private $mStartTime;
        
        /**
         * The deck with cards.
         * 
         * @var array
         */
        
        private $mDeck;
        
        /**
         * The players currently in the game.
         * 
         * @var array
         */
        
        private $mPlayers;
        
        /**
         * Whether the turns are reversed. Gets enabled when somebody plays a
         * reverse card, and more than two players are currently participating.
         * 
         * @var boolean
         */
        
        private $mReversed;
        
        /**
         * Holds the player ID of the player whose turn it currently is.
         */
        
        private $mCurrentTurn;
        
        /**
         * The top card.
         * 
         * @var Card
         */
        
        private $mTopCard;
        
        /**
         * The constructor prepares some variables we're going to need later on.
         */
        
        public function __construct ()
        {
                $this -> resetGame ();
        }
        
        /**
         * Returns the state of the engine.
         * 
         * @return The state.
         */
        
        public function getState ()
        {
                return $this -> mState;
        }
        
        /**
         * Returns whether the engine is in the given state.
         * 
         * @param int $state The state to check against.
         * @return boolean
         */
        
        public function isState ($state)
        {
                return $this -> mState == $state;
        }
        
        /**
         * Updates the state of the engine. Visibility to protected so that
         * external parties cannot change this without our consent.
         * 
         * @param int $state The state to set.
         */
        
        protected function setState ($state)
        {
                $this -> mState = $state;
        }
        
        /**
         * Sets the object that handles the output messages. Done like this so
         * that the Uno engine is easily portable.
         * 
         * @param UnoOutput $output The object handling the output.
         */
        
        public function setOutput (UnoOutput $output)
        {
                $this -> mOutput = $output;
        }
        
        /**
         * Sets the object that parses the input and calls the appropriate
         * methods on the engine.
         * 
         * @param UnoInput $input The object parsing the input.
         */
        
        public function setInput (UnoInput $input)
        {
                $this -> mInput = $input;
                $this -> mInput -> setEngine ($this);
        }
        
        /**
         * Starts the game.
         */
        
        public function startGame ()
        {
                if (count ($this -> mPlayers) < 2)
                {
                        throw new Exception (UnoStrings :: AT_LEAST_TWO_PLAYERS);
                }
                
                $this -> setState (self :: STATE_INITIALIZING);
                
                $this -> initializeDeck ();
                
                shuffle ($this -> mPlayers);
                $this -> mCurrentTurn = 0;
                
                $this -> deal ();
                
                $this -> setState (self :: STATE_PLAYING);
                $this -> mStartTime = time ();
                
                $player = $this -> currentPlayer ();
                
                $this -> mOutput -> formatPublic (UnoStrings :: GAME_STARTED);
                $this -> mOutput -> formatPublic (
                        UnoStrings :: CURRENT_TURN,
                        $player -> getNickname (),
                        $player -> numCards (),
                        $this -> mTopCard -> format ($this -> mOutput)
                );
                
                foreach ($this -> mPlayers as $player)
                {
                        $this -> mOutput -> formatPrivate (
                                $player -> getNickname (),
                                UnoStrings :: PLAYER_CARDS,
                                $player -> formatCards ($this -> mOutput)
                        );
                }
        }
        
        /**
         * Stops any running Uno game.
         */
        
        public function stopGame ()
        {
                $this -> resetGame ();
                
                $this -> mOutput -> outputPublic (UnoStrings :: GAME_STOPPED);
        }
        
        /**
         * Calculates the score of the given winning player and ends the game by
         * outputting all kinds of stuff, making the winner feel proud.
         * 
         * @param UnoPlayer $winner The winning player.
         */
        
        private function endGame (UnoPlayer $winner)
        {
                $playTime = time () - $this -> mStartTime;
                
                $losers = array ();
                $score = 0;
                foreach ($this -> mPlayers as $player)
                {
                        if ($player -> getId () == $winner -> getId ())
                                continue;
                        
                        $losers [] = $player -> getNickname ();
                        
                        $cardValue = $player -> getCardValue ();
                        $this -> mOutput -> formatPublic
                        (
                                UnoStrings :: PLAYER_HAD_CARDS,
                                $player -> getNickname (),
                                $player -> formatCards ($this -> mOutput),
                                $cardValue
                        );
                        
                        $score += $cardValue;
                }
                
                $this -> mOutput -> formatPublic
                (
                        UnoStrings :: PLAYER_HAS_WON,
                        $winner -> getNickname (),
                        Util :: formatTime ($playTime, true),
                        $score
                );
                
                $this -> mOutput -> saveScores ($score, $winner -> getNickname (), $losers);
                $this -> mOutput -> saveStatistics ($playTime, $score);
                
                $this -> resetGame ();
        }
        
        /**
         * Resets all internal variables, and cleans everything up so that the
         * Uno engine is ready for another round. 
         */
        
        private function resetGame ()
        {
                $this -> mState = self :: STATE_IDLE;
                $this -> mDeck = array ();
                $this -> mPlayers = array ();
                $this -> mReversed = false;
                $this -> mCurrentTurn = -1;
                $this -> mStartTime = null;
        }
        
        /**
         * Registers a player who wants to participate in the next round of Uno.
         * 
         * @param string $nickname The nickname the player uses on IRC.
         */
        
        public function addPlayer ($nickname)
        {
                // Update state.
                if ($this -> getState () == self :: STATE_PLAYING)
                {
                        throw new Exception (UnoStrings :: CANNOT_JOIN_WHILST_ACTIVE);
                }
                else if ($this -> getState () == self :: STATE_IDLE)
                {
                        $this -> setState (self :: STATE_PENDING);
                }
                
                if ($this -> isPlayerAdded ($nickname))
                {
                        throw new Exception (sprintf (UnoStrings :: PLAYER_ALREADY_JOINED, $nickname));
                }
                
                $id = count ($this -> mPlayers);
                $this -> mPlayers [] = new UnoPlayer ($id, $nickname);
                
                if ($this -> numPlayers () < 2)
                {
                        $this -> mOutput -> formatPublic (UnoStrings :: PLAYER_JOINED, $nickname);
                }
                else
                {
                        $this -> mOutput -> formatPublic (UnoStrings :: PLAYER_JOINED_READY, $nickname);
                }
        }
        
        /**
         * Returns whether the given player is already added to the current Uno
         * game.
         * 
         * @param string $nickname The nickname to check.
         * @return boolean
         */
        
        public function isPlayerAdded ($nickname)
        {
                foreach ($this -> mPlayers as $player)
                {
                        if ($player -> nicknameEquals ($nickname))
                        {
                                return true;
                        }
                }
                
                return false;
        }
        
        /**
         * Returns a reference to the player whose turn it is.
         * 
         * @return Player
         */
        
        private function currentPlayer ()
        {
                return $this -> mPlayers [$this -> mCurrentTurn];
        }
        
        /**
         * Returns a reference to the next player.
         * 
         * @return Player
         */
        
        private function nextPlayer ()
        {
                $nextTurn = $this -> increaseTurn ();
                return $this -> mPlayers [$nextTurn];
        }
        
        /**
         * Returns the amount of players in the game.
         * 
         * @return integer
         */
        
        public function numPlayers ()
        {
                return count ($this -> mPlayers);
        }
        
        /**
         * Initializes the deck of cards.
         */
        
        private function initializeDeck ()
        {
                $colors = array
                (
                        self :: COLOR_BLUE,
                        self :: COLOR_GREEN,
                        self :: COLOR_RED,
                        self :: COLOR_YELLOW
                );
                
                $cards = array
                (
                        self :: CARD_1,
                        self :: CARD_2,
                        self :: CARD_3,
                        self :: CARD_4,
                        self :: CARD_5,
                        self :: CARD_6,
                        self :: CARD_7,
                        self :: CARD_8,
                        self :: CARD_9,
                        self :: CARD_DRAW_2,
                        self :: CARD_REVERSE,
                        self :: CARD_SKIP
                );
                
                // Make cards.
                foreach ($colors as $color)
                {
                        new UnoDeckCard ($this, $color, self :: CARD_0);
                        
                        foreach ($cards as $card)
                        {
                                new UnoDeckCard ($this, $color, $card);
                                new UnoDeckCard ($this, $color, $card);
                        }
                }
                
                // Make wildcards.
                for ($i = 0; $i < 4; $i ++)
                {
                        new UnoDeckCard ($this, self :: COLOR_WILDCARD, self :: CARD_NONE);
                        new UnoDeckCard ($this, self :: COLOR_WILDCARD, self :: CARD_DRAW_4);
                }
                
                $this -> shuffleDeck (true);
        }
        
        /**
         * Shuffles the current deck.
         * 
         * @param boolean $silent Whether we want to show a message to the players.
         */
        
        public function shuffleDeck ($silent = false)
        {
                shuffle ($this -> mDeck);
                
                if (!$silent)
                {
                        $this -> mOutput -> outputPublic (UnoStrings :: DECK_SHUFFLED);
                }
        }
        
        /**
         * Registers an UnoDeckCard with the engine with the given properties,
         * returning a boolean indicates its success.
         * 
         * @param UnoDeckCard $card The actual card to register.
         * @param int $location The location of the card.
         * @param int $color The color of the card.
         * @param int $number The number of the card.
         * @return boolean
         */
        
        public function registerCard (UnoDeckCard $card, $location, $color, $number)
        {
                if (!$this -> isState (self :: STATE_INITIALIZING))
                {
                        return false;
                }
                
                // TODO Add more validation.
                switch ($location)
                {
                        case UnoDeckCard :: LOCATION_DECK:
                        {
                                $this -> mDeck [] = $card;
                                
                                break;
                        }
                        
                        case UnoDeckCard :: LOCATION_TOP_CARD:
                        {
                                $this -> switchTopCard ($card);
                                
                                break;
                        }
                        
                        default:
                        {
                                $this -> mPlayers [$location] -> assignCard ($card);
                                
                                break;
                        }
                }
                
                return true;
        }
        
        /**
         * Gives every player a bunch of cards and draws the first top card, to
         * get the game going.
         */
        
        private function deal ()
        {
                // No need to keep track of the coming cards.
                foreach ($this -> mPlayers as $player)
                        $player -> notifyTurnStarted ();
                
                // Deal like one would in real life.
                for ($i = 0; $i < self :: SETTING_CARDS_PER_PLAYER; $i ++)
                {
                        foreach ($this -> mPlayers as $player)
                        {
                                $this -> randomCards ($player);
                        }
                }
                
                // Reset again.
                foreach ($this -> mPlayers as $player)
                        $player -> notifyTurnPassed ();
                
                // Don't start with wildcards or other special types of cards.
                do
                {
                        $this -> switchTopCard (array_shift ($this -> mDeck));
                }
                while (
                        $this -> mTopCard -> isSameColor (self :: COLOR_WILDCARD)
                     || $this -> mTopCard -> isSameNumber (self :: CARD_DRAW_2)
                     || $this -> mTopCard -> isSameNumber (self :: CARD_REVERSE)
                     || $this -> mTopCard -> isSameNumber (self :: CARD_SKIP)
                );
        }
        
        /**
         * This method places the given card as the new open card, while putting
         * the current open card back in the deck.
         * 
         * @param UnoDeckCard|array $card The card or array of cards to place.
         */
        
        private function switchTopCard ($card)
        {
                if (is_array ($card))
                {
                        // Put all except the last back into the deck.
                        $lastIndex = count ($card) - 1;
                        for ($i = 0; $i < $lastIndex; $i ++)
                        {
                                array_push ($this -> mDeck, $card [$i]);
                                $card [$i] -> setLocation (UnoDeckCard :: LOCATION_DECK);
                        }
                        
                        // Get last card.
                        $card = $card [$lastIndex];
                }
                
                if (!($card instanceof UnoDeckCard))
                {
                        throw new Exception (UnoStrings :: INVALID_CARD);
                }
                
                if ($this -> mTopCard != null)
                {
                        // Remove the next color if it had any.
                        $this -> mTopCard -> setNextColor (null);
                        
                        // While this is equivalent to $array[], I prefer to use
                        // this function since I use array_shift() elsewhere to
                        // get a card from the deck. And since this is where I
                        // put a card back into the deck, the function call
                        // seemed a bit more like the opposite of array_shift()
                        // than some fancy syntax.
                        array_push ($this -> mDeck, $this -> mTopCard);
                        $this -> mTopCard = null;
                }
                
                $this -> mTopCard = $card;
                $card -> setLocation (UnoDeckCard :: LOCATION_TOP_CARD);
        }
        
        /**
         * Assigns one or more random cards from the deck to the given player.
         * Returns a UnoCardStack object which hold the assigned cards.
         * 
         * @param Player $player The player to assign the card to.
         * @param int $num The number of cards to assign.
         * @param bool $isDrawn Whether this card was drawn by the player's command.
         * @return UnoCardStack
         */
        
        private function randomCards (Player $player, $num = 1, $isDrawn = false)
        {
                $cards = new UnoCardStack ();
                
                for ($i = 0; $i < $num; $i ++)
                {
                        $cards -> addCard ($player -> assignCard (array_shift ($this -> mDeck), $isDrawn));
                }
                
                return $cards;
        }
        
        /**
         * Returns the index of the player in the players array who is next when
         * increased with the given number. This method incorporates whether the
         * turns are reversed or not.
         * 
         * @param int $num The number of turns to increase.
         * @return integer
         */
        
        private function increaseTurn ($num = 1)
        {
                $turn = $this -> mCurrentTurn + ($this -> mReversed ? -$num : $num);
                while ($turn < 0)
                {
                        $turn += count ($this -> mPlayers);
                }
                
                $turn %= count ($this -> mPlayers);
                
                return $turn;
        }
        
        /**
         * Gives the turn to the next player, skipping $num turns.
         * 
         * @param int $num The number of turns to skip.
         */
        
        private function advanceTurn ($num = 1)
        {
                $player = $this -> currentPlayer ();
                $player -> notifyTurnPassed ();
                
                $this -> mCurrentTurn = $this -> increaseTurn ($num);
                
                $player = $this -> currentPlayer ();
                $player -> notifyTurnStarted ();
                
                $this -> mOutput -> formatPublic (
                        UnoStrings :: CURRENT_TURN,
                        $player -> getNickname (),
                        $player -> numCards (),
                        $this -> mTopCard -> format ($this -> mOutput)
                );
                
                if ($player -> hasNewCards ())
                {
                        $this -> mOutput -> formatPrivate (
                                $player -> getNickname (),
                                UnoStrings :: PLAYER_CARDS_NEW,
                                $player -> formatCards ($this -> mOutput),
                                $player -> getNewCards () -> format ($this -> mOutput)
                        );
                }
                else
                {
                        $this -> mOutput -> formatPrivate (
                                $player -> getNickname (),
                                UnoStrings :: PLAYER_CARDS,
                                $player -> formatCards ($this -> mOutput)
                        );
                }
        }
        
        /**
         * Verifies whether the given card can be applied on top of the current
         * open card.
         * 
         * @param UnoCard $card The card to verify.
         * @return boolean
         */
        
        private function verifyCard (UnoCard $card)
        {
                if ($this -> mTopCard -> isSameColor ($card -> getColor ())
                 || $this -> mTopCard -> isSameNumber ($card -> getNumber ()))
                {
                        // Valid card.
                        return true;
                }
                else if ($this -> mTopCard -> isWildcard ()
                      && $card -> isSameColor ($this -> mTopCard -> getNextColor ()))
                {
                        // Top card is wildcard and card color matches.
                        return true;
                }
                else if ($card -> isWildcard ())
                {
                        // Wildcard fits on anything.
                        return true;
                }
                
                return false;
        }
        
        /**
         * Checks whether it's the turn of this player.
         * 
         * @return boolean
         */
        
        private function verifyTurn ($player)
        {
                return $this -> currentPlayer () -> nicknameEquals ($player);
        }
        
        /**
         * Sends a message to the given player with the status of the game as it
         * stands right now. It shows the player's current cards, the top card
         * and the number of cards the other players have.
         * 
         * @param string $player The player who requested the status.
         */
        
        public function displayStatus ($player)
        {
                
        }
        
        /**
         * Lets the given player play the given card input. Probably the most
         * important method of the whole game. It basically performs a lot of
         * checks to see whether the player plays by the rules. If so, removes
         * the card(s) from the player's hand and plays them accordingly. Also
         * applies special actions to other players if the player plays a
         * special action card.
         * 
         * @param string $player The player name.
         * @param string $cardInput The card syntax received from the player.
         */
        
        public function playCard ($player, $cardInput)
        {
                if (!$this -> verifyTurn ($player))
                {
                        return;
                }
                
                $player = $this -> currentPlayer ();
                $advanceTurns = 1;
                
                try
                {
                        $playedCard = $this -> mInput -> parseCardInput ($cardInput);
                }
                catch (Exception $ex)
                {
                        $this -> mOutput -> outputPublic ($ex -> getMessage ());
                        return;
                }
                
                if ($playedCard [self :: INDEX_INPUT] == self :: INPUT_SINGLE)
                {
                        $card = new UnoCard
                        (
                                $playedCard [self :: INDEX_COLOR],
                                $playedCard [self :: INDEX_NUMBER],
                                $playedCard [self :: INDEX_NEXT]
                        );
                        
                        if (!$player -> hasCard ($card))
                        {
                                $this -> mOutput -> formatPublic (UnoStrings :: DONT_HAVE_CARD);
                                return;
                        }
                        
                        if (!$this -> verifyCard ($card))
                        {
                                $this -> mOutput -> formatPublic (UnoStrings :: INVALID_CARD_PLAYED);
                                return;
                        }
                        
                        if ($player -> hasDrawnCard () && !$player -> getDrawnCard () -> isSameCard ($card))
                        {
                                $this -> mOutput -> formatPublic (UnoStrings :: CAN_ONLY_PLAY_DRAWN_CARD);
                                return;
                        }
                        
                        $this -> switchTopCard ($player -> playCard ($card));
                        
                        if ($card -> isWildcard ())
                        {
                                if ($playedCard [self :: INDEX_NUMBER] == self :: CARD_NONE)
                                {
                                        $this -> mOutput -> formatPublic (
                                                UnoStrings :: PLAYED_WILDCARD_CHANGED_COLOR,
                                                $player -> getNickname (),
                                                $this -> mTopCard -> format ($this -> mOutput),
                                                $this -> mOutput -> formatCardProperties ($this -> mTopCard -> getNextColor ())
                                        );
                                }
                                else
                                {
                                        // Only WD4.
                                        $this -> mOutput -> formatPublic (
                                                UnoStrings :: PLAYED_WD_CHANGED_COLOR,
                                                $player -> getNickname (),
                                                $this -> mTopCard -> format ($this -> mOutput),
                                                $this -> nextPlayer () -> getNickname (),
                                                4,
                                                $this -> mOutput -> formatCardProperties ($this -> mTopCard -> getNextColor ())
                                        );
                                }
                        }
                        
                        // Check if we need to do something special.
                        switch ($card -> getNumber ())
                        {
                                case self :: CARD_DRAW_2: // The next player draws 2 cards.
                                case self :: CARD_DRAW_4: // The next player draws 4 cards. (Nasty.)
                                {
                                        $num = $card -> isSameNumber (self :: CARD_DRAW_4) ? 4 : 2;
                                        
                                        $next = $this -> nextPlayer ();
                                        $cards = $this -> randomCards ($next, $num);
                                        
                                        if (!$card -> isWildcard ())
                                        {
                                                // Wildcard output is handled somewhere else.
                                                $this -> mOutput -> formatPublic (
                                                        UnoStrings :: PLAYED_CARD_PICK_UP,
                                                        $player -> getNickname (),
                                                        $this -> mTopCard -> format ($this -> mOutput),
                                                        $next -> getNickname (),
                                                        $num
                                                );
                                        }
                                        
                                        break;
                                }
                                
                                // The turn order reverses. Skip in case of 2 players.
                                case self :: CARD_REVERSE:
                                {
                                        if ($this -> numPlayers () > 2)
                                        {
                                                $this -> mReversed = !$this -> mReversed;
                                                
                                                $this -> mOutput -> formatPublic (
                                                        UnoStrings :: PLAYED_CARD_REVERSED,
                                                        $player -> getNickname (),
                                                        $this -> mTopCard -> format ($this -> mOutput)
                                                );
                                                
                                                break;
                                        }
                                        
                                        // Deliberate fall-through because reverse with 2 players
                                        // acts the same as a skip.
                                }
                                
                                // The next player is skipped.
                                case self :: CARD_SKIP:
                                {
                                        $next = $this -> nextPlayer ();
                                        $this -> mOutput -> formatPublic (
                                                UnoStrings :: PLAYED_CARD_SKIPPED_ONE,
                                                $player -> getNickname (),
                                                $this -> mTopCard -> format ($this -> mOutput),
                                                $next -> getNickname ()
                                        );
                                        
                                        $advanceTurns = 2;
                                        break;
                                }
                                
                                // Moving right along.
                                default:
                                {
                                        if (!$card -> isWildcard ())
                                        {
                                                // Wildcard output is handled somewhere else.
                                                $this -> mOutput -> formatPublic (
                                                        UnoStrings :: PLAYED_CARD,
                                                        $player -> getNickname (),
                                                        $this -> mTopCard -> format ($this -> mOutput)
                                                );
                                        }
                                        
                                        break;
                                }
                        }
                }
                else if ($playedCard [self :: INDEX_INPUT] == self :: INPUT_STACK)
                {
                        if ($playedCard [self :: INDEX_COLOR] == self :: COLOR_WILDCARD)
                        {
                                $this -> mOutput -> formatPublic (UnoStrings :: INVALID_COLOR_SUPPLIED);
                                return;
                        }
                        
                        $firstCard = null;
                        
                        if ($player -> hasDrawnCard ())
                        {
                                // Check if the drawn card can be applied on top.
                                $firstCard = $player -> getDrawnCard ();
                                if (!$this -> verifyCard ($firstCard))
                                {
                                        $this -> mOutput -> formatPublic (UnoStrings :: STACK_BEGIN_WITH_DRAWN_CARD);
                                        return;
                                }
                        }
                        else if ($this -> mTopCard -> isSameNumber ($playedCard [self :: INDEX_NUMBER]))
                        {
                                // All is cool already, but put the same color in front, if the player has such a card.
                                $card = new UnoCard
                                (
                                        $this -> mTopCard -> getColor (),
                                        $playedCard [self :: INDEX_NUMBER]
                                );
                                
                                if ($player -> hasCard ($card))
                                {
                                        $firstCard = $card;
                                }
                        }
                        else
                        {
                                // Check color.
                                $color = $this -> mTopCard -> getColor ();
                                if ($this -> mTopCard -> isSameColor (self :: COLOR_WILDCARD))
                                {
                                        $color = $this -> mTopCard -> getNextColor ();
                                }
                                
                                $firstCard = new UnoCard ($color, $playedCard [self :: INDEX_NUMBER]);
                                
                                if (!$player -> hasCard ($firstCard))
                                {
                                        $this -> mOutput -> formatPublic (
                                                UnoStrings :: DONT_HAVE_SPECIFIC_CARD,
                                                $firstCard -> format ($this -> mOutput)
                                        );
                                        return;
                                }
                        }
                        
                        $lastCard = new UnoCard
                        (
                                $playedCard [self :: INDEX_COLOR],
                                $playedCard [self :: INDEX_NUMBER]
                        );
                        
                        if (!$player -> hasCard ($lastCard))
                        {
                                $this -> mOutput -> formatPublic (
                                        UnoStrings :: DONT_HAVE_SPECIFIC_CARD,
                                        $lastCard -> format ($this -> mOutput)
                                );
                                
                                return;
                        }
                        else if ($firstCard !== null && $firstCard -> isSameCard ($lastCard))
                        {
                                // Two of the same cards. Does the player have two?
                                if ($player -> numCards ($firstCard) < 2)
                                {
                                        $this -> mOutput -> formatPublic (UnoStrings :: STACK_CARD_SUPPLIED_AT_FRONT);
                                        return;
                                }
                        }
                        
                        $stack = array ();
                        if ($firstCard !== null)
                        {
                                $stack [] = $player -> playCard ($firstCard);
                        }
                        
                        $stack = array_merge ($stack, $player -> playStack (
                                $playedCard [self :: INDEX_NUMBER],
                                $playedCard [self :: INDEX_COLOR]
                        ));
                        $this -> switchTopCard ($stack);
                        
                        // Check if we need to do something special.
                        switch ($lastCard -> getNumber ())
                        {
                                case self :: CARD_DRAW_2: // The next player draws 2 cards.
                                case self :: CARD_DRAW_4: // The next player draws 4 cards. (Nasty.)
                                {
                                        $num = $lastCard -> isSameNumber (self :: CARD_DRAW_4) ? 4 : 2;
                                        $num *= count ($stack);
                                        
                                        $next = $this -> nextPlayer ();
                                        $cards = $this -> randomCards ($next, $num);
                                        
                                        $this -> mOutput -> formatPublic (
                                                UnoStrings :: PLAYED_CARD_PICK_UP,
                                                $player -> getNickname (),
                                                UnoCardStack :: formatArray ($this -> mOutput, $stack),
                                                $next -> getNickname (),
                                                $num
                                        );
                                        
                                        break;
                                }
                                
                                // The turn order reverses. Skip in case of 2 players.
                                case self :: CARD_REVERSE:
                                {
                                        if ($this -> numPlayers () > 2)
                                        {
                                                if (count ($stack) % 2 == 1)
                                                        $this -> mReversed = !$this -> mReversed;
                                                
                                                $this -> mOutput -> formatPublic (
                                                        UnoStrings :: PLAYED_CARD_REVERSED,
                                                        $player -> getNickname (),
                                                        UnoCardStack :: formatArray ($this -> mOutput, $stack)
                                                );
                                        }
                                        else
                                        {
                                                $next = $this -> nextPlayer ();
                                                $this -> mOutput -> formatPublic (
                                                        UnoStrings :: PLAYED_CARD_SKIPPED_ONE,
                                                        $player -> getNickname (),
                                                        UnoCardStack :: formatArray ($this -> mOutput, $stack),
                                                        $next -> getNickname ()
                                                );
                                                
                                                $advanceTurns = 2;
                                        }
                                        
                                        break;
                                }
                                
                                // The next player is skipped.
                                case self :: CARD_SKIP:
                                {
                                        if ($this -> numPlayers () > 2)
                                        {
                                                $num = count ($stack);
                                                $this -> mOutput -> formatPublic (
                                                        UnoStrings :: PLAYED_CARD_SKIPPED_MULTIPLE,
                                                        $player -> getNickname (),
                                                        UnoCardStack :: formatArray ($this -> mOutput, $stack),
                                                        $num
                                                );
                                                
                                                $advanceTurns = $num + 1;
                                        }
                                        else
                                        {
                                                $next = $this -> nextPlayer ();
                                                $this -> mOutput -> formatPublic (
                                                        UnoStrings :: PLAYED_CARD_SKIPPED_ONE,
                                                        $player -> getNickname (),
                                                        UnoCardStack :: formatArray ($this -> mOutput, $stack),
                                                        $next -> getNickname ()
                                                );
                                                
                                                $advanceTurns = 2;
                                        }
                                        
                                        break;
                                }
                                
                                // Moving right along.
                                default:
                                {
                                        $this -> mOutput -> formatPublic (
                                                UnoStrings :: PLAYED_CARD,
                                                $player -> getNickname (),
                                                UnoCardStack :: formatArray ($this -> mOutput, $stack)
                                        );
                                        
                                        break;
                                }
                        }
                }
                else
                {
                        throw new Exception (UnoStrings :: UNEXPECTED_CONDITION);
                }
                
                switch ($player -> numCards ())
                {
                        case 1:
                        {
                                $this -> mOutput -> formatPublic (
                                        UnoStrings :: PLAYER_HAS_UNO,
                                        $player -> getNickname ()
                                );
                                break;
                        }
                        
                        case 0:
                        {
                                // This player won, calculate scores and wind down the game.
                                $this -> endGame ($player);
                                
                                // We're done here.
                                return;
                        }
                }
                
                $this -> advanceTurn ($advanceTurns);
        }
        
        /**
         * Lets the player draw a card for when no moves are possible. This can
         * only be done once, and if the player cannot apply the drawn card
         * either, the turn has to be passed.
         * 
         * @param string $player The player in action.
         */
        
        public function drawCard ($player)
        {
                if (!$this -> verifyTurn ($player))
                {
                        return;
                }
                
                $player = $this -> currentPlayer ();
                
                if (!$player -> hasDrawnCard ())
                {
                        $cards = $this -> randomCards ($player, 1, true);
                        
                        $this -> mOutput -> formatPublic (
                                UnoStrings :: DREW_CARD,
                                $player -> getNickname ()
                        );
                        
                        $this -> mOutput -> formatPrivate (
                                $player -> getNickname (),
                                UnoStrings :: PLAYER_CARDS_NEW,
                                $player -> formatCards ($this -> mOutput),
                                $cards -> format ($this -> mOutput)
                        );
                }
                else
                {
                        $this -> mOutput -> formatPublic (UnoStrings :: ALREADY_DREW_CARD);
                }
        }
        
        /**
         * Lets a player pass their turn. However, before that is allowed, the
         * player must have drawn a card from the deck.
         * 
         * @param string $player The player in action.
         */
        
        public function passTurn ($player)
        {
                if (!$this -> verifyTurn ($player))
                {
                        return;
                }
                
                $player = $this -> currentPlayer ();
                
                if ($player -> hasDrawnCard ())
                {
                        $this -> mOutput -> formatPublic (
                                UnoStrings :: PASSED_TURN,
                                $this -> mPlayers [$this -> mCurrentTurn] -> getNickname ()
                        );
                        
                        $this -> advanceTurn ();
                }
                else
                {
                        $this -> mOutput -> formatPublic (UnoStrings :: ONLY_PASS_AFTER_DRAW);
                }
        }
}